package santiago.server;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import santiago.codebase.IClient;
import santiago.codebase.IServer;
import santiago.codebase.NetworkParameter;
import santiago.codebase.Notification;
import santiago.domain.CardinalPoint;
import santiago.domain.DifficultyLevel;
import santiago.domain.Game;
import santiago.domain.Player;
import santiago.exception.AlreadyInGameException;
import santiago.exception.ExistingItemException;
import santiago.exception.GameAboardedException;
import santiago.exception.InvalidAmountException;
import santiago.exception.MaximalPlayerCountReachedException;
import santiago.exception.NoSuchGameException;
import santiago.exception.NotEnoughCashException;
import santiago.exception.NotEnoughPlayerException;
import santiago.exception.NotInGameException;

public class Server {

    private static IServer Proxy;
    private static Map<String, Game> Games;
    private static Map<String, IClient> ConnectedClients;
    private static Map<String, Player> Players;

    public Server() {
        initProxy();
        Games = new HashMap<>();
        ConnectedClients = new HashMap<>();
        Players = new HashMap<>();
    }

    /**
     * Allow clients connection
     */
    private static void initProxy() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    //Gets local IP address
                    String LocalIP = InetAddress.getLocalHost().getHostAddress();

                    //Display parameters
                    System.out.println("Server name: " + (String) NetworkParameter.SERVER_NAME.getValue());
                    System.out.println("Server port: " + (Integer) NetworkParameter.SERVER_PORT.getValue());
                    System.out.println("RMI port: " + (Integer) NetworkParameter.RMI_PORT.getValue());
                    System.out.println("Local IP: " + LocalIP);

                    Proxy = (IServer) UnicastRemoteObject.exportObject(new RMIServer(), (Integer) NetworkParameter.SERVER_PORT.getValue());
                    LocateRegistry.createRegistry((Integer) NetworkParameter.RMI_PORT.getValue()).rebind("//" + LocalIP + ":" + (Integer) NetworkParameter.RMI_PORT.getValue() + "/" + (String) NetworkParameter.SERVER_NAME.getValue(), Proxy);

                    //Server is started
                    System.out.println("\nServer is up!");
                    System.out.println("Waiting for request...\n");
                } catch (UnknownHostException | RemoteException ex) {
                    System.out.println("\nStart-up failed!");
                }
            }
        }).start();
    }

    public static void main(String[] args) {
        new Server();
    }

    /**
     * Add a player to the server if he's name isn't already used
     *
     * @param c
     * @throws ExistingItemException
     */
    public static void addClient(String Login, IClient c) throws ExistingItemException {
        if (ConnectedClients.containsKey(Login)) {
            throw new ExistingItemException();
        }
        ConnectedClients.put(Login, c);
        Players.put(Login, new Player(Login));
        ServerMessage.Connected.display(Login, null);
    }

    /**
     * Removes a client from the server
     *
     * @param Login Client's login
     */
    public static void removeClient(String Login) {
        Game PlayingGame = getPlayerGame(Login);
        if (PlayingGame != null) {
            leaveGame(Login, PlayingGame.getName());
        }
        ConnectedClients.remove(Login);
        Players.remove(Login);
        ServerMessage.Disconnected.display(Login, null);
    }

    public static void createGame(String GameName, int PlayerNbr, DifficultyLevel Level, boolean DisplayCash, boolean PalmTree) throws ExistingItemException {
        Game g = new Game(GameName, PlayerNbr, Level, DisplayCash, PalmTree);
        addGame(g);
        ServerMessage.CreateGame.display(GameName, null);
    }

    /**
     * Add a game to the server if it doesn't contains any game of the same name
     *
     * @param g
     * @throws ExistingItemException
     */
    private static void addGame(Game g) throws ExistingItemException {
        if (Games.containsKey(g.getName())) {
            throw new ExistingItemException();
        }
        Games.put(g.getName(), g);
    }

    /**
     * @param GameName
     * @return The corresponding <code>Game</code> or <code>null</code> if not
     * found
     */
    public static Game getGame(String GameName) {
        return Games.get(GameName);
    }

    public static void joinGame(String GameName, String Login) throws NoSuchGameException, ExistingItemException, MaximalPlayerCountReachedException, AlreadyInGameException, RemoteException, NotEnoughPlayerException {
        Game Game = getGame(GameName);
        if (Game == null) {
            throw new NoSuchGameException();
        } else {
            synchronized (Game) {
                Game.addPlayer(getPlayer(Login));
                ServerMessage.JoinedGame.display(Login, GameName);
                if (Game.isFull()) {
                    notifyAll(GameName, Notification.START_GAME);
                    Game.start();
                    if (Game.getParameters().getPlayerCount() < 5) {
                        sendSystemMessage(Game, Game.getDeletedPlantation() + " a été supprimée");
                    }
                    sendSystemMessage(Game, Game.getCurrentPlayer().getName() + " commence !");
                } else {
                    notifyAll(GameName, Notification.JOIN_GAME);
                }
            }
        }
    }

    public static void leaveGame(String Login, String GameName) {
        try {
            Game Game = getGame(GameName);
            Game.removePlayer(Login);
            if (!Game.hasPlayer()) {
                removeGame(GameName);
            } else {
                notifyAll(GameName, Notification.QUIT_GAME);
            }
            ServerMessage.LeaveGame.display(Login, GameName);
        } catch (GameAboardedException ex) {
            Server.aboardGame(GameName);
        }
    }

    private static void aboardGame(String GameName) {
        Game Aboarded = getGame(GameName);
        notifyAll(GameName, Notification.ABOARD_GAME);
        Aboarded.aboard();
        removeGame(GameName);
        ServerMessage.AboardGame.display(GameName, null);
    }

    public static void bid(String Login, int Amount) throws RemoteException, InvalidAmountException, NotEnoughCashException {
        Game Game = getPlayerGame(Login);
        Game.bid(Login, Amount);
        if (!Game.isStageOver()) {
            notifyAll(Notification.PLAYER_ACTION);
        } else {
            sendSystemMessage(Game, Game.getCanalOverseer().getName() + " est le nouveau constructeur de canal !");
            notifyAll(Notification.LAST_PLAYER_ACTION);
        }
    }

    public static void placePlantation(String Login, int NodeIndex, int PlantationIndex, CardinalPoint Location) throws RemoteException {
        Game Game = getPlayerGame(Login);
        Game.placePlantation(Login, NodeIndex, PlantationIndex, Location);
        if (!Game.isStageOver()) {
            notifyAll(Notification.PLAYER_ACTION);
        } else {
            notifyAll(Notification.LAST_PLAYER_ACTION);
        }
    }

    private static void removeGame(String GameName) {
        Games.remove(GameName);
        ServerMessage.DeletedGame.display();
    }

    public static void sendMessage(String Login, String Message) {
        try {
            Player p = getPlayer(Login);
            notifyAll(p.getGameName(), Notification.PLAYER_MESSAGE, "<div><span style='color:" + p.getColor().toCSSColor() + ";'>" + Login + " :</span> " + Message + "</div>");
        } catch (NotInGameException ex) {
            //Aboard: the player is not currently in a game
        }
    }

    private static void sendSystemMessage(Game Game, String Message) {
        notifyAll(Game.getName(), Notification.SYSTEM_MESSAGE, "<div style='font-style:italic;'>" + Message + "</div>");
    }

    public static Collection<Player> getGamePlayers(String GameName) {
        Game Game = getGame(GameName);
        if (Game != null) {
            return Game.getPlayers();
        }
        return new ArrayList<>();
    }

    public static Player getPlayer(String Login) {
        return Players.get(Login);
    }

    /**
     * Returns only a set of joinable games
     *
     * @return Game set
     */
    public static Set<Game> getAvailableGames() {
        Set<Game> AvailableGames = new HashSet<>();
        for (Map.Entry<String, Game> entry : Games.entrySet()) {
            Game game = entry.getValue();
            if (!game.isStarted()) {
                AvailableGames.add(game);
            }
        }
        ServerMessage.ListGame.display();
        return AvailableGames;
    }

    private static void notify(final IClient Client, final Notification Notification) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Client.notify(Notification);
                } catch (RemoteException ex) {
                    removeDisconnectedClients(Client);
                }
            }
        }).start();
    }

    private static void notify(final IClient Client, final Notification Notification, final Object Data) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Client.notify(Notification, Data);
                } catch (RemoteException ex) {
                    removeDisconnectedClients(Client);
                }
            }
        }).start();
    }

    private static void notifyAll(final Notification Notification) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (IClient Client : ConnectedClients.values()) {
                    Server.notify(Client, Notification);
                }
            }
        }).start();
    }

    private static void notifyAll(final Notification Notification, final Object Data) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (IClient Client : ConnectedClients.values()) {
                    Server.notify(Client, Notification, Data);
                }
            }
        }).start();
    }

    private static void notifyAll(final String GameName, final Notification Notification) {
        for (Iterator<Player> it = getGame(GameName).getPlayers().iterator(); it.hasNext();) {
            Server.notify(ConnectedClients.get(it.next().getName()), Notification);
        }
    }

    private static void notifyAll(final String GameName, final Notification Notification, final Object Data) {
        for (Iterator<Player> it = getGame(GameName).getPlayers().iterator(); it.hasNext();) {
            Server.notify(ConnectedClients.get(it.next().getName()), Notification, Data);
        }
    }

    /**
     * Removes a client from the server
     *
     * Also removes all disconnected clients
     *
     * @param Login Client's login
     */
    protected synchronized static void removeDisconnectedClients(IClient Client) {
        for (Map.Entry<String, IClient> entry : ConnectedClients.entrySet()) {
            try {
                removeClient(entry.getValue().getGameName());
            } catch (RemoteException ex) {
                removeClient(entry.getKey());
            }
        }
    }

    /**
     * Ask a lot of efforts to the server!
     *
     * @param Login Player's name
     * @return
     */
    public static Game getPlayerGame(String Login) {
        for (Map.Entry<String, Game> entry : Games.entrySet()) {
            Game game = entry.getValue();
            for (Iterator<Player> it = game.getPlayers().iterator(); it.hasNext();) {
                Player player = it.next();
                if (player.getName().equals(Login)) {
                    return game;
                }
            }
        }
        return null;
    }
}
